use super::env::{env_get, env_keys, env_set, function_env};
use super::expand::expand;
use super::string;
use super::{parse_eval, Env, Err, Exp, Function};
use crate::could_not;

use crate::api::fs;
use crate::{ensure_length_eq, ensure_length_gt, expected};

use alloc::boxed::Box;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;

fn eval_quote_args(args: &[Exp]) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    Ok(args[0].clone())
}

fn eval_atom_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    match eval(&args[0], env)? {
        Exp::List(_) => Ok(Exp::Bool(false)),
        _ => Ok(Exp::Bool(true)),
    }
}

fn eval_equal_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 2);
    let a = eval(&args[0], env)?;
    let b = eval(&args[1], env)?;
    Ok(Exp::Bool(a == b))
}

fn eval_head_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    match eval(&args[0], env)? {
        Exp::List(l) => {
            ensure_length_gt!(l, 0);
            Ok(l[0].clone())
        }
        Exp::Str(s) => {
            ensure_length_gt!(s, 0);
            Ok(Exp::Str(s.chars().next().unwrap().to_string()))
        }
        _ => expected!("first argument to be a list or a string"),
    }
}

fn eval_tail_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    match eval(&args[0], env)? {
        Exp::List(list) => {
            ensure_length_gt!(list, 0);
            Ok(Exp::List(list[1..].to_vec()))
        }
        Exp::Str(s) => {
            ensure_length_gt!(s, 0);
            Ok(Exp::Str(s.chars().skip(1).collect()))
        }
        _ => expected!("first argument to be a list or a string"),
    }
}

fn eval_cons_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 2);
    match eval(&args[1], env)? {
        Exp::List(mut list) => {
            list.insert(0, eval(&args[0], env)?);
            Ok(Exp::List(list))
        }
        _ => expected!("first argument to be a list"),
    }
}

fn eval_is_variable_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    match &args[0] {
        Exp::Sym(name) => {
            Ok(Exp::Bool(env_get(name, env).is_ok()))
        }
        _ => expected!("first argument to be a symbol"),
    }
}

pub fn eval_variable_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 2);
    match &args[0] {
        Exp::Sym(name) => {
            let exp = eval(&args[1], env)?;
            env.borrow_mut().data.insert(name.clone(), exp);
            Ok(Exp::Sym(name.clone()))
        }
        _ => expected!("first argument to be a symbol"),
    }
}

fn eval_mutate_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 2);
    match &args[0] {
        Exp::Sym(name) => {
            let exp = eval(&args[1], env)?;
            Ok(env_set(name, exp, env)?)
        }
        _ => expected!("first argument to be a symbol"),
    }
}

fn eval_env_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 0);
    let keys = env_keys(env)?.iter().map(|k| Exp::Sym(k.clone())).collect();
    Ok(Exp::List(keys))
}

fn eval_while_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_gt!(args, 1);
    let cond = &args[0];
    let mut res = Exp::List(vec![]);
    while eval(cond, env)?.is_truthy() {
        for arg in &args[1..] {
            res = eval(arg, env)?;
        }
    }
    Ok(res)
}

fn eval_apply_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_gt!(args, 1);
    let mut args = args.to_vec();
    match eval(&args.pop().unwrap(), env) {
        Ok(Exp::List(rest)) => args.extend(rest),
        _ => return expected!("last argument to be a list"),
    }
    eval(&Exp::List(args.to_vec()), env)
}

fn eval_eval_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    let exp = eval(&args[0], env)?;
    eval(&exp, env)
}

fn eval_do_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    let mut res = Ok(Exp::List(vec![]));
    for arg in args {
        res = Ok(eval(arg, env)?);
    }
    res
}

fn eval_load_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    let path = string(&eval(&args[0], env)?)?;
    let mut input = fs::read_to_string(&path).
        or(could_not!("read file '{}'", path))?;
    loop {
        let (rest, _) = parse_eval(&input, env)?;
        if rest.is_empty() {
            break;
        }
        input = rest;
    }
    Ok(Exp::Bool(true))
}

fn eval_doc_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Exp, Err> {
    ensure_length_eq!(args, 1);
    match eval(&args[0], env)? {
        Exp::Primitive(_) => Ok(Exp::Str("".to_string())),
        Exp::Function(f) => Ok(Exp::Str(f.doc.unwrap_or("".to_string()))),
        Exp::Macro(m) => Ok(Exp::Str(m.doc.unwrap_or("".to_string()))),
        _ => expected!("function or macro"),
    }
}

pub fn eval_args(
    args: &[Exp],
    env: &mut Rc<RefCell<Env>>
) -> Result<Vec<Exp>, Err> {
    args.iter().map(|x| eval(x, env)).collect()
}

pub const BUILT_INS: [&str; 27] = [
    "quote",
    "quasiquote",
    "unquote",
    "unquote-splicing",
    "atom?",
    "equal?",
    "head",
    "tail",
    "cons",
    "if",
    "cond",
    "while",
    "function",
    "variable",
    "variable?",
    "mutate",
    "macro",
    "define-function",
    "define",
    "define-macro",
    "apply",
    "eval",
    "expand",
    "do",
    "load",
    "doc",
    "env",
];

pub fn eval(exp: &Exp, env: &mut Rc<RefCell<Env>>) -> Result<Exp, Err> {
    let mut exp = exp;
    let mut env = env;
    let mut env_tmp;
    let mut exp_tmp;
    loop {
        match exp {
            Exp::Sym(key) => return env_get(key, env),
            Exp::Bool(_) => return Ok(exp.clone()),
            Exp::Num(_) => return Ok(exp.clone()),
            Exp::Str(_) => return Ok(exp.clone()),
            Exp::List(list) => {
                ensure_length_gt!(list, 0);
                let args = &list[1..];
                match &list[0] {
                    Exp::Sym(s) if s == "quote" => {
                        return eval_quote_args(args);
                    }
                    Exp::Sym(s) if s == "atom?" => {
                        return eval_atom_args(args, env);
                    }
                    Exp::Sym(s) if s == "equal?" => {
                        return eval_equal_args(args, env);
                    }
                    Exp::Sym(s) if s == "head" => {
                        return eval_head_args(args, env);
                    }
                    Exp::Sym(s) if s == "tail" => {
                        return eval_tail_args(args, env);
                    }
                    Exp::Sym(s) if s == "cons" => {
                        return eval_cons_args(args, env);
                    }
                    Exp::Sym(s) if s == "while" => {
                        return eval_while_args(args, env);
                    }
                    Exp::Sym(s) if s == "apply" => {
                        return eval_apply_args(args, env);
                    }
                    Exp::Sym(s) if s == "eval" => {
                        return eval_eval_args(args, env);
                    }
                    Exp::Sym(s) if s == "do" => {
                        return eval_do_args(args, env);
                    }
                    Exp::Sym(s) if s == "load" => {
                        return eval_load_args(args, env);
                    }
                    Exp::Sym(s) if s == "doc" => {
                        return eval_doc_args(args, env);
                    }
                    Exp::Sym(s) if s == "variable?" => {
                        return eval_is_variable_args(args, env);
                    }
                    Exp::Sym(s) if s == "variable" => {
                        return eval_variable_args(args, env);
                    }
                    Exp::Sym(s) if s == "mutate" => {
                        return eval_mutate_args(args, env);
                    }
                    Exp::Sym(s) if s == "env" => {
                        return eval_env_args(args, env);
                    }
                    Exp::Sym(s) if s == "expand" => {
                        ensure_length_eq!(args, 1);
                        return expand(&args[0], env);
                    }
                    Exp::Sym(s) if s == "if" => {
                        ensure_length_gt!(args, 1);
                        if eval(&args[0], env)?.is_truthy() { // Consequent
                            exp_tmp = args[1].clone();
                        } else if args.len() > 2 { // Alternate
                            exp_tmp = args[2].clone();
                        } else { // '()
                            exp_tmp = Exp::List(vec![
                                Exp::Sym("quote".to_string()),
                                Exp::List(vec![]),
                            ]);
                        }
                        exp = &exp_tmp;
                    }
                    Exp::Sym(s) if s == "function" || s == "macro" => {
                        let (params, body, doc) = match args.len() {
                            2 => {
                                (args[0].clone(), args[1].clone(), None)
                            }
                            3 => {
                                let doc = Some(string(&args[1])?);
                                (args[0].clone(), args[2].clone(), doc)
                            }
                            _ => return expected!("3 or 4 arguments"),
                        };
                        let f = Box::new(Function { params, body, doc });
                        let exp = if s == "function" {
                            Exp::Function(f)
                        } else {
                            Exp::Macro(f)
                        };
                        return Ok(exp);
                    }
                    _ => match eval(&list[0], env)? {
                        Exp::Function(f) => {
                            env_tmp = function_env(&f.params, args, env)?;
                            exp_tmp = f.body;
                            env = &mut env_tmp;
                            exp = &exp_tmp;
                        }
                        Exp::Primitive(f) => {
                            return f(&eval_args(args, env)?);
                        }
                        _ => {
                            return expected!("first argument to be a function");
                        }
                    },
                }
            }
            _ => return Err(Err::Reason("Unexpected argument".to_string())),
        }
    }
}
